/*
 * Copyright 2009 XueSong Guo.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package cn.webwheel.utils;

import cn.webwheel.Action;
import cn.webwheel.Main;
import cn.webwheel.ResultInterpreter;
import cn.webwheel.WebContext;
import cn.webwheel.template.ComponentRenderer;
import cn.webwheel.template.ComponentRendererBuilder;
import cn.webwheel.template.plain.PlainTemplateParser;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.HashMap;
import java.util.Map;

/**
 * 页面模板结果的解释器。
 */
public class TemplateResultInterpreter implements ResultInterpreter<TemplateResult>, ComponentRenderer {

    private Main main;

    private int cacheTTL;

    private String templateCharset;
    private String renderCharset;
    private String contentType;

    private Map<CID, RendererInfo> renderers = new HashMap<CID, RendererInfo>();

    private Map<Class, String[]> templates = new HashMap<Class, String[]>();

    /**
     * 构造方法
     * @param main 应用程序主类
     */
    public TemplateResultInterpreter(Main main) {
        this(main, "utf-8");
    }

    /**
     * 构造方法
     * @param main 应用程序主类
     * @param charset 页面模板文件和页面渲染的默认编码
     */
    public TemplateResultInterpreter(Main main, String charset) {
        this(main, 5000, charset, charset, "text/html");
    }

    /**
     * 构造方法
     * @param main 应用程序主类
     * @param cacheTTL 页面模板文件更新检查的间隔时间，毫秒为单位，默认为5000，负数为不检查页面模板文件的更新情况
     * @param templateCharset 页面模板文件的默认编码
     * @param renderCharset 渲染页面所使用的默认编码
     * @param contentType 渲染页面所使用的默认mime内容类型
     */
    public TemplateResultInterpreter(Main main, int cacheTTL, String templateCharset, String renderCharset, String contentType) {
        if (main == null || templateCharset == null || renderCharset == null || contentType == null) {
            throw new NullPointerException();
        }
        this.main = main;
        this.cacheTTL = cacheTTL;
        this.templateCharset = templateCharset;
        this.renderCharset = renderCharset;
        this.contentType = contentType;
    }

    /**
     * 绑定一个类型到一个页面模板文件地址，绑定后，动作类可以通过使用{@link cn.webwheel.utils.TemplateResult#TemplateResult(Class, Object)}返回对应页面模板结果。
     * @param cls 页面组件类型
     * @param template 页面模板文件地址，为相对应用web根目录的绝对地址，如“/index.html”、“/WEB-INF/some/dir/somepage.template”
     */
    public void bindTemplate(Class cls, String template) {
        bindTemplate(cls, template, null);
    }

    /**
     * 绑定一个类型到一个页面模板文件地址，绑定后，动作类可以通过使用{@link cn.webwheel.utils.TemplateResult#TemplateResult(Class, Object)}返回对应页面模板结果。
     * @param cls 页面组件类型
     * @param template 页面模板文件地址，为相对应用web根目录的绝对地址，如“/index.html”、“/WEB-INF/some/dir/somepage.template”
     * @param templateCharset 页面模板文件的编码
     */
    public void bindTemplate(Class cls, String template, String templateCharset) {
        if (cls == null) {
            throw new NullPointerException();
        }
        if (template == null) {
            templates.remove(cls);
        } else {
            templates.put(cls, new String[]{template, templateCharset});
        }
    }

    public void init() throws Exception {
    }

    /**
     * 执行页面模板渲染
     * @param result 页面模板结果
     * @param ctx 应用程序上下文
     * @param action 动作类实例
     * @return 是否处理了此次http请求
     * @throws Exception 任何异常
     */
    @SuppressWarnings("unchecked")
    public boolean render(TemplateResult result, WebContext ctx, Action action) throws Exception {
        HttpServletResponse response = ctx.getResponse();
        String contentType = result.getContentType();
        if(contentType==null) contentType = this.contentType;
        String renderCharset = result.getRenderCharset();
        if(renderCharset==null) renderCharset = this.renderCharset;
        response.setContentType(contentType);
        response.setCharacterEncoding(renderCharset);
        render(response.getWriter(), result);
        return true;
    }

    public void destroy() {
    }

    /**
     * 作为外部组件的代理渲染方法
     * @param writer 输出流
     * @param component 页面组件实例
     * @param delegate 代理渲染器
     * @throws IOException 输入输出异常
     */
    public void render(Writer writer, Object component, ComponentRenderer delegate) throws IOException {
        if (component instanceof TemplateResult) {
            render(writer, (TemplateResult) component);
        } else if (component != null) {
            String[] ss = templates.get(component.getClass());
            if (ss == null) {
                writer.write(component.toString());
            } else {
                render(writer, new TemplateResult(component));
            }
        }
    }

    /**
     * 渲染页面模板结果到输出流
     * @param writer 输出流
     * @param result 页面组件结果
     * @throws IOException 输入输出异常
     */
    @SuppressWarnings("unchecked")
    public void render(Writer writer, TemplateResult result) throws IOException {
        Class cls = result.getCls();
        String path = result.getPath();
        String templateCharset = result.getTemplateCharset();
        if (path == null) {
            String[] ss = templates.get(cls);
            if (ss == null) {
                throw new IOException("can not find template file for " + cls);
            }
            path = ss[0];
            if (templateCharset == null) templateCharset = ss[1];
        }
        CID cid = new CID(path, cls);
        RendererInfo info = renderers.get(cid);

        if (info != null && cacheTTL >= 0) {
            long time = System.currentTimeMillis();
            if (time - info.lastTestTime > cacheTTL) {
                File file = new File(main.getServletContext().getRealPath(path));
                long lm = file.lastModified();
                if (lm != info.lastModified) {
                    info = null;
                } else {
                    info.lastTestTime = time;
                }
            }
        }
        if (info == null) {
            File file = new File(main.getServletContext().getRealPath(path));

            if(templateCharset==null) {
                templateCharset = this.templateCharset;
            }

            InputStreamReader reader = new InputStreamReader(new FileInputStream(file), templateCharset);

            char[] buf = new char[8192];
            int rd;
            StringBuilder sb = new StringBuilder();

            try {
                while ((rd = reader.read(buf)) != -1) {
                    sb.append(buf, 0, rd);
                }
            } finally {
                reader.close();
            }

            info = new RendererInfo();
            try {
                ComponentRendererBuilder builder = new ComponentRendererBuilder(cls, sb.toString());
                info.renderer = (ComponentRenderer) builder.build().newInstance();
            } catch (PlainTemplateParser.TemplateParserException e) {
                throw new IOException("can not parse template: " + path, e);
            } catch(RuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new IOException(e);
            }
            info.lastModified = file.lastModified();
            if (cacheTTL >= 0) {
                info.lastTestTime = System.currentTimeMillis();
            }
            renderers.put(cid, info);
        }
        Object com = result.getCom();
        if(com==null) com = main.getInstanceOf(cls);
        info.renderer.render(writer, com, this);
    }

    private static class CID {
        String path;
        Class cls;

        private CID(String path, Class cls) {
            this.path = path;
            this.cls = cls;
        }

        public boolean equals(Object o) {
            if (o == null || getClass() != o.getClass()) return false;
            CID cid = (CID) o;
            return cls==cid.cls && path.equals(cid.path);
        }

        public int hashCode() {
            int result = path.hashCode();
            result = 31 * result + cls.hashCode();
            return result;
        }
    }

    private static class RendererInfo {
        ComponentRenderer renderer;
        long lastModified;
        long lastTestTime;
    }
}
